﻿/*
Copyright (c) 2007-2011 William POTTIER - AllProgrammic

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights 
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 
of the Software, and to permit persons to whom the Software is furnished to do so, 
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all 
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net.Sockets;
using System.Threading;
using NetAccess.Common;
using NetAccess.Formatters;
using NetAccess.Message;

namespace NetAccess.Server
{
    /// <summary>
    /// Represesent a client connected to the server
    /// </summary>
    /// <typeparam name="T">ServerLayer implementation</typeparam>
    public class ServerConnexion<T> : BaseServerConnexion, IContextLogger
        where T : BaseServerLayer
    {
        #region IContextLogger
        public string GetName()
        {
            return ToString();
        }
        #endregion

        #region Fields
        // Remote Socket
        private Socket _remoteSocket;

        // 
        private ManualResetEvent _waitReceive;
        private ManualResetEvent _waitPong;

        // State of the connexion
        private ServerConnexionsState _state;
        
        // Public key for asymetrical encryption
        private byte[] _publicKey;

        // Message formatter
        private MessageFormatter _messageFormatter;

        #endregion

        /// <summary>
        /// Server instance link with this ServerConnexion
        /// </summary>
        public T Server { get; private set; }

        internal override void InternalInitialize(BaseServerLayer server, Socket remote)
        {
            if (!(server is T))
                throw new ArgumentException("Bad server type", "server");

            Id = GetHashCode();
            _remoteSocket = remote;
            _remoteSocket.NoDelay = true;
            Server = (T)server;

            _messageFormatter = new MessageFormatter(_remoteSocket.SendBufferSize);
            
            _state = ServerConnexionsState.Connecting;

            // On démarre la boucle de reception
            ThreadPool.QueueUserWorkItem(ListenIncommingDataTCP);

            OnInitialize();
        }

        /// <summary>
        /// Method call when initializing this class. Do nothing, override it in subclass
        /// </summary>
        protected virtual void OnInitialize() { }

        #region Connexion
        private void DoHandShake(InternalMessage message)
        {
            if (_state != ServerConnexionsState.Connecting)
                return;

            // Store public key
            _publicKey = message.MessageData[0] as byte[];

            var additionalsData = new object[0];
            if(message.MessageData.Length >= 2)
                 additionalsData = message.MessageData[1] as object[];

            if (!HandShake(additionalsData))
            {
                this.ConnectRejectConnexion("Handshake failed");
                Disconnect();
                return;
            }

            ConnectAccept();

            _state = ServerConnexionsState.Connected;
            Server.ClientConnect(this);
            OnConnected();
        }

        /// <summary>
        /// Override to implement custom handshake process
        /// </summary>
        /// <param name="data"></param>
        /// <returns>return true to validate handshake; false to reject.</returns>
        protected virtual bool HandShake(object[] data) { return true; }

        /// <summary>
        /// Call to accept the connexion
        /// </summary>
        internal void ConnectAccept()
        {
            InternalSend(new InternalMessage(
                             InternalMessageIdentifier.ServerConnection,
                             ConnectionResult.Accept,
                             Server.Configuration.ServerName,
                             Server.Configuration.ApplicationIdentifier,
                             Server.Configuration.IsEnableCompression),
                         true);
        }

        /// <summary>
        /// Call to refuse connexion and send a ServerFull response to client
        /// </summary>
        internal void ConnectServerFull()
        {
            InternalSend(new InternalMessage(
                             InternalMessageIdentifier.ServerConnection,
                             ConnectionResult.ServerFull,
                             Server.Configuration.ServerName,
                             Server.Configuration.MaxConnexions),
                         true);
        }

        /// <summary>
        /// Call to refuse connexion and send a custom response to client
        /// </summary>
        internal void ConnectRejectConnexion(string message)
        {
            InternalSend(new InternalMessage(
                             InternalMessageIdentifier.ServerConnection,
                             ConnectionResult.Reject,
                             Server.Configuration.ServerName,
                             message),
                         true);
        }

        /// <summary>
        /// Method call when client is connected
        /// </summary>
        protected virtual void OnConnected() { }
        #endregion

        #region Disconnexion
        internal override void Disconnect()
        {
            lock (this)
            {
                if (_state != ServerConnexionsState.Connected && _state != ServerConnexionsState.Connecting) return;

                var raise = false;
                if (_state == ServerConnexionsState.Connected)
                    raise = true;


                _state = ServerConnexionsState.Disconnecting;

                if (_remoteSocket != null)
                    _remoteSocket.Close();

                if (_waitReceive != null)
                {
                    _waitReceive.Set();
                    _waitReceive.Close();
                }

                if (!raise) return;

                OnDisconnected();
                Server.OnClientDisconnected(this);

                _state = ServerConnexionsState.Disconnected;
            }
        }

        /// <summary>
        /// Method call when client is disconnected
        /// </summary>
        protected virtual void OnDisconnected() { }
        #endregion

        #region Reception
        /// <summary>
        /// Listen loop of incomming data
        /// </summary>
        /// <param name="state"></param>
        private void ListenIncommingDataTCP(object state)
        {
            _waitReceive = new ManualResetEvent(false);

            while(_state == ServerConnexionsState.Connecting || _state == ServerConnexionsState.Connected)
            {
                try
                {
                    _waitReceive.Reset();
                }
                catch (ObjectDisposedException) { break; }
                catch (NullReferenceException) { break; }

                try
                {
                    var bufferRecv = new byte[_remoteSocket.ReceiveBufferSize];
                    _remoteSocket.BeginReceive(
                        bufferRecv,
                        0,
                        _remoteSocket.ReceiveBufferSize,
                        SocketFlags.None,
                        ReceiveTCPCallback,
                        bufferRecv);
                }
                catch (ObjectDisposedException) { break; }
                catch (NullReferenceException) { break; }
                catch (SocketException se)
                {
                    if (se.SocketErrorCode == SocketError.ConnectionReset) { Disconnect(); }
                    else
                    {
                        Logger.Instance.Error(se, this);
                    }
                }

                try
                {
                    _waitReceive.WaitOne();
                }
                catch (ObjectDisposedException) { break; }
                catch (NullReferenceException) { break; }
            }

            Logger.Instance.Debug("End of the incomming message loop", this);
        }

        /// <summary>
        /// Listen callback
        /// </summary>
        /// <param name="ar"></param>
        private void ReceiveTCPCallback(IAsyncResult ar)
        {
            try
            {
                _waitReceive.Set();
            }
            catch (ObjectDisposedException) { return; }
            catch (NullReferenceException) { return; }

            var numread = 0;
            var buffer = new byte[0];

            try
            {
                numread = _remoteSocket.EndReceive(ar);
                buffer = (byte[])ar.AsyncState;
            }
            catch (ObjectDisposedException) { return; }
            catch (NullReferenceException) { return; }
            catch (SocketException se)
            {
                if (se.SocketErrorCode == SocketError.ConnectionReset) { Disconnect(); }
                else
                {
                    Logger.Instance.Error(se, this);
                }
            }

            if (numread <= 0) return;

            var ms = new System.IO.MemoryStream(buffer, 0, numread);
            _messageFormatter.AddDataToReception(ms.ToArray(), numread);
            ms.Close();

            ProcessingIncommingData();
        }

        private void ProcessingIncommingData()
        {
            Queue<InternalMessage> messages = _messageFormatter.ProcessIncommingMessages();

            while (messages.Count > 0)
            {
                InternalMessage message = messages.Dequeue();
                switch (message.MessageIdentifier)
                {
                    case InternalMessageIdentifier.HandShake:
                        Logger.Instance.Debug("Receive Handshake data");
                        DoHandShake(message);
                        break;
                    case InternalMessageIdentifier.Ping:
                        // Receive a ping from the client then send him a pong
                        InternalSend(new InternalMessage(InternalMessageIdentifier.Pong), false);
                        break;
                    case InternalMessageIdentifier.Pong:
                        // Receive pong, calculate ping time
                        _waitPong.Set();
                        break;
                    case InternalMessageIdentifier.TransmitData:
                        // Receive application data
                        DataReceived((NetworkMessage)message.MessageData[0]);
                        break;
                    default:
                        Logger.Instance.Error("Receiving invalid data (unknown or invalid InternalMessageIdentifier)");
                        break;
                }
            }
        }

        /// <summary>
        /// Handle incomming of application data
        /// </summary>
        /// <param name="message">Application Data</param>
        protected virtual void DataReceived(NetworkMessage message) { }
        #endregion

        #region Sending

        /// <summary>
        /// Broadcast a message to all client on the server except this one
        /// </summary>
        /// <param name="message">Message to send</param>
        /// <param name="selfExclude">If true exclude current connexion from recipients</param>
        /// <param name="encrypt">If true encrypt the message</param>
        protected void Broadcast(NetworkMessage message, bool selfExclude = true, bool encrypt = false)
        {
            Server.InternalBroadcast(
                new InternalMessage(InternalMessageIdentifier.TransmitData, message),
                encrypt,
                (selfExclude ? this : null));
        }

        /// <summary>
        /// Broadcast a message to all client in a room except this one
        /// </summary>
        /// <param name="roomIdentifier">The room unique identifier</param>
        /// <param name="message">Message to send</param>
        /// <param name="selfExclude">If true exclude current connexion from recipients</param>
        /// <param name="encrypt">If true encrypt the message</param>
        protected void BroadcastRoom(int roomIdentifier, NetworkMessage message, bool selfExclude = true, bool encrypt = false)
        {
            Server.InternalBroadcastRoom(
                roomIdentifier,
                new InternalMessage(InternalMessageIdentifier.TransmitData, message),
                encrypt,
                (selfExclude ? this : null));
        }

        /// <summary>
        /// Send message to this client
        /// </summary>
        /// <param name="message">Message to send</param>
        /// <param name="encrypt">If true encrypt the message</param>
        protected void Send(NetworkMessage message, bool encrypt = false)
        {
            InternalSend(new InternalMessage(InternalMessageIdentifier.TransmitData, message), false, encrypt);
        }

        /// <summary>
        /// Send message to this client
        /// </summary>
        /// <param name="message">Message to send</param>
        /// <param name="escapeCompression">Set to true to force disable compression (ie : ignore compression configuration)</param>
        /// <param name="encrypt">If true encrypt the message</param>
        internal override void InternalSend(InternalMessage message, bool escapeCompression, bool encrypt = false)
        {
            if (_state != ServerConnexionsState.Connecting && _state != ServerConnexionsState.Connected)
                return;

            if (encrypt)
            {
                if (_publicKey == null)
                    throw new Exception("Impossible d'envoyer un message crypté à un client qui n'a pas transmit sa clef public");
            }

            var packets = _messageFormatter.Prepare(
                message,
                (Server.Configuration.IsEnableCompression && !escapeCompression),
                (encrypt ? _publicKey : null));
            Send(packets);
        }

        /// <summary>
        /// LSend bytes array through socket
        /// </summary>
        /// <param name="packets">Queue of bytes array to send</param>
        private void Send(Queue<byte[]> packets)
        {
            while (packets.Count > 0)
            {
                byte[] buffer = packets.Dequeue();
                _remoteSocket.BeginSend(buffer, 0, buffer.Length, SocketFlags.None, SendCallback, buffer);
            }
        }

        /// <summary>
        /// Callback of socket async send
        /// </summary>
        /// <param name="result">IAsyncResult</param>
        private void SendCallback(IAsyncResult result)
        {
            try
            {
                _remoteSocket.EndSend(result);
            }
            catch(Exception ex)
            {
                Logger.Instance.Error(ex, this);
            }
        }
        #endregion

        #region Ping
        internal long Ping(int timeout)
        {
            if(_waitPong == null)
                _waitPong = new ManualResetEvent(false);

            // Ping message
            var message = new InternalMessage(InternalMessageIdentifier.Ping);
            var datas = _messageFormatter.Prepare(message);

            // Start watch
            var watch = new Stopwatch();
            watch.Start();

            // Send message
            Send(datas);

            // Wait for response
            var pong = _waitPong.WaitOne(timeout, false);

            // Stop watch
            watch.Stop();

            // Get ping time if success
            return (pong ? watch.ElapsedMilliseconds : -1);
        }
        #endregion

        #pragma warning disable 659
        /// <summary>
        /// Compare ServerConnexion based on ID
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        public override bool Equals(object obj)
        {
            var o = obj as BaseServerConnexion;
            if (o == null)
                return false;
            return Id == o.Id;
        }
        #pragma warning restore 659

        public override string ToString()
        {
            return String.Format("ServerConnexion#{0}", Id);
        }
    }
}
